查看原文
其他

VR进化论|教你搭建通用的WebVR工程

YorkChan 前端外刊评论 2022-07-03

感谢 @YorkChan 的投稿。本文旨在介绍如何搭建WebVR工程以支持多场景开发。

首先,作为一个基本的前端工程来说,我们需要让代码“工程化”,不仅要提供编译构建、压缩打包功能,还要让每个页面模块化; 延伸到WebVR工程,我们也需要考虑“多页面”模块化,即提供多个场景模块化开发,因为一个完整的WebVR App不仅仅只有一个场景。这里可以参考google的WebVR多场景示例:

 webvr多场景应用

多场景开发,最简单的方式就是,一个场景对应一份html、css、js,多个页面需要多个html,每次页面跳转需要重新进行VR渲染进行初始化。 实际上我们在多场景中,场景初始化只需要执行一次(比如,创建一个场景->创建相机->创建渲染器),我们只需要一个index.html作为入口页面,将VR场景初始化、创建、回收、切换封装成公用组件。


WebVR场景切换,用户的耐心是有限的

在首次进入场景时进行初始化,在需要场景切换时进行场景回收和按需加载,这样一来,用户切换场景时,不用把时间浪费在等待html和初始化场景上。基于以上思路,本人总结的一套WebVR工程搭建方案,供各位参考。

项目地址: Demo: 相关技术栈:three.js、webpack2、es6/7 想详细了解WebVR开发步骤,也欢迎参考我的文章

实现功能

  • VR多场景模块化开发

  • 支持VR场景创建、回收、切换

  • 项目自动化构建与压缩打包

  • 支持es7/6

WebVR相关库

  • three.js

  • vrcontrols.js

  • vreffect.js

  • webvr-manager.js

  • webvr-polyfill.js

  • three-onevent.js

主要目录结构

  1. webpack

  2. |-- webpack.config.js       # 公共配置

  3. |-- webpack.dev.js          # 开发配置

  4. |-- webpack.prod.js         # 生产配置

  5. src                         # 项目源码

  6. |-- page                    # WebVR场景目录      

  7. |   |-- index.js            # WebVR入口场景              

  8. |   |-- page1.js

  9. |   |-- page2.js                                            

  10. |-- common                  # 公共目录,包括webvr封装类和polyfill

  11. |   |-- VRCore.js

  12. |   |-- VRPage.js

  13. |   |-- vendor.js

  14. |-- lib                     # vr三方插件,包括相机控制器和分屏器

  15. |   |-- vrcontrol.js

  16. |   |-- vreffect.js

  17. |-- assets                  # 素材目录,包括3d模型、纹理、音频等

  18. |   |-- audio                      

  19. |   |-- model

  20. |   |-- texture

  21. |-- index.html              # WebVR公用页面

  22. package.json                        

  23. READNE.md

我们先来看看index.html,其实整个body就只有一个dom,用来append我们的canvas,毕竟所以场景都在canvas里运行。

  1. <!DOCTYPE html>

  2. <html lang="en">

  3. <head>

  4.    <meta charset="UTF-8">

  5.    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">

  6.    <title>webVR-INDEX</title>

  7. </head>

  8. <body>

  9.  <section class="webvr-container">

  10.  </section>

  11. </body>

  12. </html>

有了公用html,我们希望这样开发WebVR应用,即一个场景对应一个js脚本,形如:

  1. // 继承VRPage父类,开发每一个场景

  2. import VRPage from 'common/js/VRPage';

  3. class Page1 extends VRPage {

  4.    start() { // 启动渲染之前,创建场景3d模型

  5.        let geometry = new THREE.CubeGeometry(5,5,5);

  6.        let material = new THREE.MeshBasicMaterial( { color:0x00aadd} );

  7.        this.box = new THREE.Mesh(geometry,material);

  8.        this.box.position.set(3,-2,-3);

  9.        WebVR.Scene.add(this.box);

  10.    }

  11.    loaded() { // 场景资源加载完毕,可执行音频播放等。

  12.    }

  13.    update(delta) { // 开启渲染之后,执行模型动画

  14.        this.box.rotation.y += 0.05;

  15.    }

  16. }

  17. export default (() => {

  18.    return new Page1();

  19. })();

这里参照了类似Unity3d和React的开发模式,在start方法里创建3d模型,在update方法里处理3d动画,这样的好处在于:

  1. 每一个场景都可以进行独立开发而互不影响;

  2. 一旦VR环境初始化之后,不需要在每次场景跳转切换时重新初始化一遍。


WebVR多场景运行机制

VRCore.js作为公用模块管理整个webvr应用的所有子场景,包括场景初始化、VR相机渲染、场景切换、场景回收等静态函数。 VRPage.js作为每个场景的工厂类,支持不同3d页面(场景)之间的代码独立。 每一个VR页面的生命周期都是:创建物体->加载模型->启动渲染的过程,因此,需要创建一个基类,来实现每一个VR场景实例的生命周期。

  1. //common/VRPage.js

  2. import * as WebVR from 'VRCore.js' //管理所有场景的公用模块

  3. // VR场景工厂

  4. export default class VRPage {

  5.    constructor(options={}) {

  6.        // 创建场景,如果场景已初始化

  7.        WebVR.createScene(options);

  8.        this.start();

  9.        this.loadPage();

  10.    }

  11.    loadPage() {

  12.        THREE.DefaultLoadingManager.onLoad = () => {

  13.            // 模型加载完毕,即开启渲染

  14.            WebVR.renderStart(this.update);

  15.            this.loaded();

  16.        }

  17.    }

  18.    start() {

  19.         // 实例的start方法将在启动渲染之前,场景相机初始化后执行。

  20.    }

  21.    loaded() {

  22.        // 实例的loaded方法将在场景资源加载后执行。

  23.    }

  24.    update(delta) {

  25.        // 实例的update方法将在渲染器每一次渲染时执行。

  26.    }

  27. }

这里使用THREE.DefaultLoadingManager.onLoad方法监听场景是否加载完毕,一旦加载完毕,便启动渲染。

WebVR场景首次渲染

主要包括四个步骤

  1. 新建场景

  2. 创建VR相机

  3. 加载场景脚本与资源

  4. 开启动画渲染

VR环境初始化

  1. function createScene({domContainer=document.body,fov=70,far=4000}) {

  2.    // 创建场景

  3.    Scene = new THREE.Scene();

  4.    // 创建相机

  5.    Camera = new THREE.PerspectiveCamera(fov,window.innerWidth/window.innerHeight,0.1,far);

  6.    Camera.position.set( 0, 0, 0 );

  7.    Scene.add(Camera);

  8.    // 创建渲染器

  9.    Renderer = new THREE.WebGLRenderer({ antialias: true } );

  10.    Renderer.setSize(window.innerWidth,window.innerHeight);

  11.    Renderer.shadowMapEnabled = true;

  12.    Renderer.setPixelRatio(window.devicePixelRatio);

  13.    domContainer.appendChild(Renderer.domElement);

  14.    initVR();

  15.    resize();

  16. }

首先是three.js开发三部曲,创建场景、相机、渲染器,接着调用initVR函数来完成VR场景分屏和陀螺仪控制,WebVR基本开发步骤可以参考。

  1. function initVR() {

  2.    // 初始化VR分屏器和控制器

  3.    Effect = new THREE.VREffect(Renderer);

  4.    Controls = new THREE.VRControls(Camera);

  5.    // 初始化VR管理器

  6.    Manager = new WebVRManager(Renderer, Effect);

  7.    window.addEventListener( 'resize', e => {

  8.        // 调整渲染器和相机以适应窗口拉伸时宽高变动

  9.        Camera.aspect = window.innerWidth / window.innerHeight;

  10.        Camera.updateProjectionMatrix();

  11.        Effect.setSize(window.innerWidth, window.innerHeight);

  12.    }, false );

  13. }

开启动画渲染

  1. // VRCore.js

  2. function renderStart(callback) {

  3.    // 设置loopID变量记录每一帧ID

  4.    loopID = 0;

  5.    const loop = () => {

  6.        if(loopID === -1) return;

  7.        loopID = requestAnimationFrame(loop);

  8.        callback();

  9.        Controls.update();

  10.        Manager.render(Scene, Camera);

  11.    };

  12.    loop();

  13. }

这里传入参数动画渲染做了三件事,使用loopID作为整个VR应用的全局变量,记录每一帧动画的更新;更新相机控制器和VR渲染器,

WebVR场景切换

主要包括四个步骤

  1. 暂停渲染

  2. 清空当前场景物体

  3. 请求并加载目标场景脚本与资源

  4. 重启渲染

暂停动画渲染

  1. function renderStop() {

  2.    if (loopID !== -1) {

  3.        window.cancelAnimationFrame(loopID);

  4.        loopID = -1;

  5.    }

  6. }

回收当前场景

  1. function clearScene() {

  2.    for(let i = Scene.children.length - 1; i >= 0; i-- ) {

  3.        Scene.remove(Scene.children[i]);

  4.    }

  5. }

按需加载 切换到下一场景,我们需要请求对应的场景脚本,这里使用webpack2的函数进行代码分离,当然你也可以使用require.ensure(filename => {require(filename)})方法。

  1. import(`page/${fileName}.js`);

最终将清空当前场景与请求加载目标场景功能封装为forward跳转方法,就可以在页面里直接调用了。

  1. // common/VRCore.js

  2. function forward(fileName) {

  3.    renderStop();

  4.    clearScene();

  5.    import(`page/${fileName}.js`);

  6. }

  7. // page/index.js

  8. ...

  9. class Index extends VRPage {

  10.    start() {

  11.        let geometry = new THREE.CubeGeometry(5,5,5);

  12.        let material = new THREE.MeshBasicMaterial({

  13.            color: 0x00aadd

  14.        });

  15.        this.box = new THREE.Mesh(geometry,material);

  16.        this.box.position.set(3,-2,-3);

  17.        // add gaze eventLisenter

  18.        this.box.on('gaze',mesh => { // gazeIn trigger

  19.            WebVR.forward('page2.js');

  20.        });

  21.        WebVR.Scene.add(box);

  22.    }

  23. }

  24. ...

  25. // page2.js

  26. class page2 extends VRPage {

  27.    start() {

  28.        this.addPanorama(1000, ASSET_TEXTURE_SKYBOX);

  29.    }

  30.    addPanorama(radius,path) {

  31.        // create panorama

  32.        let geometry = new THREE.SphereGeometry(radius,50,50);

  33.        let material = new THREE.MeshBasicMaterial( { map: new THREE.TextureLoader().load(path),side:THREE.BackSide } );

  34.        let panorama = new THREE.Mesh(geometry,material);

  35.        WebVR.Scene.add(panorama);

  36.        return panorama;

  37.    }

  38. }

  39. export default (() => {

  40.    return new page2();

  41. })();

我们在场景里创建一个立方体,当凝视到该物体时,执行forward方法跳转至page2场景。

至此,我们的WebVR工程已经完成了一半,接下来,我们使用Webpack2来构建我们的工程。

webpack配置

开发环境和生产环境下webpack配置略有不同,这里主要给出webpack的基本配置,具体可参考项目地址。

  1. const path = require('path');

  2. const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

  3. const HtmlWebpackPlugin = require('html-webpack-plugin');

  4. const ProvidePlugin = require('webpack/lib/ProvidePlugin');

  5. module.exports = {

  6.  entry: {

  7.    'vendor': './src/common/js/vendor.js',

  8.    'app': './src/page/index.js'

  9.  },

  10.  output: {

  11.    path: path.resolve(__dirname, '../dist/'),

  12.    filename: '[name].js',

  13.    sourceMapFilename: '[name].map',

  14.    chunkFilename: '[id]-chunk.js',

  15.    publicPath: '/'

  16.  },

这里我们将webvr首个场景src/page/index.js作为项目打包入口,同时将page目录下的文件也作为单独chunk,配合按需加载来支持场景切换。

  1.  module: {

  2.    rules: [

  3.      {

  4.        test: /\.js/,

  5.        exclude: /node_modules/,

  6.        use: [

  7.          { loader:'babel-loader',options: {

  8.            presets: ["latest",["es2015", {"modules": false}]]

  9.          }

  10.        ]

  11.      },

  12.      {

  13.        test: /\.css/,

  14.        use: ['style-loader','css-loader']

  15.      },

  16.      {

  17.        test: /\.(jpg|png|mp4|wav|ogg|obj|mtl|dae)$/,

  18.        loader: 'file-loader'

  19.      }

  20.    ]

  21.  },

这里引入file-loader,这样就能在场景里直接import需要用到的素材,如下。

  1. //page/page2.js

  2. import ASSET_TEXTURE_SKYBOX from 'assets/texture/360_page2.jpg';

webpack相关的plugin配置如下

  1.  plugins: [

  2.    new CommonsChunkPlugin({

  3.      name: ['app', 'vendor'],

  4.      minChunks: Infinity

  5.    }),

  6.    new ProvidePlugin({

  7.      'THREE': 'three',

  8.      'WebVR': path.resolve(__dirname,'../src/common/js/VRCore.js')

  9.    }),

  10.    new HtmlWebpackPlugin({

  11.      inject: true,

  12.      template: path.resolve(__dirname, '../src/index.html'),

  13.      favicon: path.resolve(__dirname, '../src/favicon.ico')

  14.    })

  15.  ]

  16. };

使用ProvidePlugin将three.js作为公用模块输出,以省去在每个脚本import THREE from 'three'的重复工作,同时将管理所有场景的核心模块VRCore.js作为全局公用模块输出。 使用HtmlWebpackPlugin将公用的html打包到dist目录下。

polyfill配置

最后是polyfill配置,我们需要引入webvr-polyfill和babel-polyfill来分别支持webvr API和ES6 API,并作为一个页面独立脚本。

  1. // common/vendor.js

  2. import 'babel-polyfill';

  3. import 'webvr-polyfill';

以上WebVR工程已经基本搭建完毕,欢迎各位提出宝贵意见,后续我们将探索daydream和Oculus在webvr上的开发模式,敬请期待。

最后,献上前几天在google开发者网站上看到的:预测未来,不如创造未来。


每周一篇值得关注的前端美文,欢迎订阅!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存